<template>
  <div class="scroll-wrapper" ref="wrapper">
    <div class="scroll-content" ref="scrollWrapper">
      <slot></slot>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { nextTick, ref, onMounted, onBeforeUnmount } from "vue";

const props = defineProps({
  direction: {
    type: String,
    default: "up",
    validator: (val: string) => ["left", "right", "up", "down"].includes(val)
  },
  step: {
    type: Number,
    default: 1 // 每帧滚动多少像素
  },
  pauseOnHover: {
    type: Boolean,
    default: true
  },
  scrollOnWheel: {
    type: Boolean,
    default: true // 监听鼠标滚动的事件
  },
  slidesPerView: {
    type: Number,
    default: 1 // 每次显示几条
  },
  itemGap: {
    type: Number,
    default: 2 // 每项之间的间距（单位 px）
  }
});

const wrapper = ref();
const scrollWrapper = ref<any>();

let isHovering = false;
let scrollAmount = 0;
let animationFrameId = 0;
const COPY_NUM = 2;

// copy一份内容，防止循环一半切换时视觉空白
const duplicateContent = () => {
  const originalChildren = Array.from(scrollWrapper.value.children);
  // 先清空可能存在的克隆元素
  const clones = scrollWrapper.value.querySelectorAll(".cloned");
  clones.forEach((clone: any) => clone.remove());

  // 复制内容
  originalChildren.forEach((child: any) => {
    const clone = child.cloneNode(true);
    clone.classList.add("cloned");
    scrollWrapper.value.appendChild(clone);
  });
};

// 边界情况处理，尤其在鼠标滚动的时候
const resetIfOutOfBounds = () => {
  const sw = scrollWrapper.value;
  const horizontal = ["left", "right"].includes(props.direction);
  const scrollSize = horizontal ? sw.scrollWidth : sw.scrollHeight;
  const clientSize = horizontal ? sw.clientWidth : sw.clientHeight;
  const halfLimit = scrollSize / COPY_NUM;
  const maxScroll = scrollSize - clientSize;
  if (["left", "up"].includes(props.direction)) {
    if (scrollAmount >= halfLimit || scrollAmount <= 0) {
      scrollAmount = 0;
    }
  } else {
    const minScroll = maxScroll - halfLimit;
    if (scrollAmount >= maxScroll || scrollAmount <= minScroll) {
      scrollAmount = maxScroll;
    }
  }
};
const scroll = () => {
  if (!(props.pauseOnHover && isHovering)) {
    if (["left", "up"].includes(props.direction)) {
      scrollAmount += props.step;
    } else {
      scrollAmount -= props.step;
    }
  }
  if (props.direction === "left" || props.direction === "right") {
    scrollWrapper.value.style.transform = `translateX(${-scrollAmount}px)`;
  } else {
    scrollWrapper.value.style.transform = `translateY(${-scrollAmount}px)`;
  }
  resetIfOutOfBounds();
  animationFrameId = requestAnimationFrame(scroll);
};

const onMouseEnter = () => (isHovering = true);
const onMouseLeave = () => (isHovering = false);

const onWheel = (e: any) => {
  if (!props.scrollOnWheel) return;
  e.preventDefault();
  const delta = e.deltaY || e.deltaX;
  scrollAmount += delta;
};

const setItemSize = () => {
  const wrapperSize =
    props.direction === "left" || props.direction === "right" ? wrapper.value?.clientWidth : wrapper.value?.clientHeight;
  const itemSize = (wrapperSize - props.itemGap * (props.slidesPerView - 1)) / props.slidesPerView;

  const isVertical = ["up", "down"].includes(props.direction);
  scrollWrapper.value.style.flexDirection = isVertical ? "column" : "row";
  scrollWrapper.value.style.gap = props.itemGap + "px";

  const items = Array.from(scrollWrapper.value.children).filter((el: any) => !el.classList.contains("cloned"));
  Array.from(items).forEach((el: any) => {
    if (isVertical) {
      el.style.height = itemSize + "px";
    } else {
      el.style.width = itemSize + "px";
    }
  });
};
const setInitialScrollAmount = () => {
  if (props.direction === "left") {
    scrollAmount = 0;
  } else if (props.direction === "up") {
    scrollAmount = 0;
  } else if (props.direction === "down") {
    scrollAmount = scrollWrapper.value.scrollHeight - scrollWrapper.value.clientHeight;
  } else if (props.direction === "right") {
    scrollAmount = scrollWrapper.value.scrollWidth - scrollWrapper.value.clientWidth;
  }
};

onMounted(() => {
  nextTick(() => {
    setItemSize();
    duplicateContent();
    setInitialScrollAmount();
    wrapper.value.addEventListener("mouseenter", onMouseEnter);
    wrapper.value.addEventListener("mouseleave", onMouseLeave);
    if (props.scrollOnWheel) {
      wrapper.value.addEventListener("wheel", onWheel, { passive: false });
    }
    animationFrameId = requestAnimationFrame(scroll);
  });
});

onBeforeUnmount(() => {
  cancelAnimationFrame(animationFrameId);
  wrapper.value.removeEventListener("mouseenter", onMouseEnter);
  wrapper.value.removeEventListener("mouseleave", onMouseLeave);
  if (props.scrollOnWheel) {
    wrapper.value.removeEventListener("wheel", onWheel);
  }
});
</script>

<style scoped>
.scroll-wrapper {
  overflow: hidden;
  width: 100%;
  height: 100%;
}

.scroll-content {
  display: flex;
  width: 100%;
  height: 100%;
}

.scroll-content > * {
  width: 100%;
  height: 100%;
}
</style>
